Skip to content

fix(bridge): fail fast when a request is discarded by a Messages relaunch - #199

Merged
steipete merged 4 commits into
openclaw:mainfrom
omarshahine:omarshahine/bridge-fail-fast
Aug 2, 2026
Merged

fix(bridge): fail fast when a request is discarded by a Messages relaunch#199
steipete merged 4 commits into
openclaw:mainfrom
omarshahine:omarshahine/bridge-fail-fast

Conversation

@omarshahine

@omarshahine omarshahine commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

Fixes an issue where a bridge request in flight when Messages.app restarts blocks the caller for the full timeout instead of failing. For sends that is 150s of a completely unresponsive caller, ending in a timeout for a request that was actually discarded seconds in.

Seen in production on an OpenClaw gateway: the agent's iMessage reply blocked for 150s and surfaced as a failure, even though the bridge had been repaired ~23s in and was healthy again long before the timeout expired.

Why This Change Was Made

MessagesLauncher.launchInjectedMessages() calls cleanQueueDirectory() on both queue directories, so relaunching Messages.app with the dylib wipes any request already in flight. When Messages.app dies mid-request and the keepalive relaunches it, the original request file is deleted and nothing will ever write its response. invokeV2 had no way to notice, so it polled the outbox until the deadline for a reply with no writer.

The request's own on-disk state is a precise signal. A live request is either unclaimed (<id>.json) or claimed by the dylib (<id>.processing.<pid>, set by processV2InboxFile). Neither present, with no reply, means the queue was cleared.

Two details worth review:

  • Race with reply-writing. The dylib removes its claim and writes the reply as separate steps, so a reply can land between the two checks. The outbox is re-checked once before giving up.
  • Fail-safe direction. An inbox that cannot be enumerated returns "still queued" so a transient read error never aborts a live request. The change can only end a wait early when the request is provably absent.

Beyond ending the stall, this separates "discarded, definitely not delivered" (.bridgeNotReady) from a plain timeout, where delivery is genuinely unknown. That distinction is what lets a caller retry safely: retrying after a timeout risks a duplicate, retrying a discarded request cannot.

User Impact

A send issued while Messages.app is restarting now fails in about a poll interval instead of blocking for 150s, and reports a cause the caller can act on. Callers that today surface a hard failure to the user can distinguish a discarded request and retry it once the bridge is back.

Evidence

Pre-fix production incident. Timeline reconstructed from macOS unified logs and the gateway log:

Time (local) Event
21:16:29 Messages.app (pid 7836) alive, serving the bridge
21:16:58.587 loginwindow … appDeath for com.apple.MobileSMS — Messages.app dies
21:17:45 send-message issued; request written to the inbox
21:17:42Z keepalive logs private bridge unavailable; reinjecting
21:17:54.807 Messages.app relaunches as pid 31647 (cold start: AppSandbox, container_create_or_lookup) — queue directories wiped here
21:18:05Z keepalive logs private bridge ready
21:20:16 caller finally errors, Timed out waiting for response to 'send-message'

The bridge was healthy from 21:18:05, yet the request waited another ~2 minutes for a reply that could not arrive because its request file no longer existed. That is the window this change closes.

Tests (Tests/IMsgCoreTests/IMsgBridgeClientQueueTests.swift) cover the three on-disk shapes the loop distinguishes, plus the fail-safe:

  • unclaimed <id>.json → still queued
  • claimed <id>.processing.<pid> → still queued (a live request must not be aborted)
  • neither, including an unrelated request present → not queued
  • unreadable inbox → still queued

swift test: 489 tests in 4 suites, all passing. swift format lint clean on both touched files; swiftlint reports no violations in them; git diff --check clean.

Proof gap, stated plainly: the exact-path runtime evidence is the pre-fix incident. I have not killed Messages.app mid-send against the patched build — doing that on the production gateway would interrupt a live messaging channel. The discard path is covered deterministically by the tests above.

AI-assisted.


After-fix real behavior proof (added)

Run on macOS 26 against the real injected Messages.app, with the production gateway stopped first so its imsg rpc released the bridge and the RPC queue had no competing client (verified imsg rpc count 0, inbox empty before the run).

Fault injection. SIGSTOP on the injected Messages process (verified state T) so the dylib physically cannot drain the inbox, then the request is issued, then the process is killed and imsg launch relaunches it — which is what invokes cleanQueueDirectory() and wipes the in-flight request.

Early failure — the changed path, patched build:

request queued: EA6D821C-2857-40DD-8BB7-8B99F80D93A2.json
elapsed=11.658s
errorCase=imsg bridge not ready: request for 'typing' was discarded before it was
          processed (Messages.app restarted or the bridge queue was cleared)

The call was made with timeout: 150. Before this change it would have polled the full 150s and then reported Timed out waiting for response to 'send-message'; instead it returns .bridgeNotReady as soon as the request is observed to be gone. The elapsed time is dominated by client startup before the request is written; the wait ends promptly once the queue is wiped.

Recovery — same call after the bridge is back:

elapsed=0.067s
errorCase=Dylib error: Missing required parameter: handle

The dylib answers in 67ms with its own parameter validation, confirming the bridge recovered and that the new queue-state check does not produce a false positive against a live bridge.

These were produced with a small throwaway executable calling IMsgBridgeClient.invoke directly, because each CLI subcommand wraps bridge errors in its own message (imsg send falls back to AppleScript and surfaces Connection is invalid (-609); imsg typing surfaces an imagent error), which hides the underlying IMsgBridgeError. The probe is not part of this PR — the branch contains only the two files above.

Supporting CLI timings from the same isolated setup, showing the wait ending rather than running to the deadline: imsg send returned 0.52s after its request was queued, imsg typing 0.415s, and a post-recovery send succeeded in 0.64s.

Environment restored: production LaunchAgent bootstrapped, listener on :18789, exactly one imsg rpc, gateway health ok, iMessage channel running with lastError: null and reconnectAttempts: 0, and a live round-trip send verified.

Remaining honesty note: the crash that starts this sequence is not addressed here. On the affected machine Messages.app dies on its own roughly every 2-3 days (6 message-tool stalls across 14 days of gateway logs); this change only stops an in-flight request from waiting out the full timeout when that happens.

…unch

A v2 request that disappears from the inbox without a reply can never be
answered, but the poll loop kept waiting for it until the caller's full
timeout. For sends that is 150s, during which the caller is blocked.

`MessagesLauncher.launchInjectedMessages()` calls `cleanQueueDirectory()`
on both queue directories, so relaunching Messages.app with the dylib wipes
any request already in flight. If Messages.app dies mid-request — the
keepalive then relaunches and reinjects — the original request is deleted
and nothing will ever write its response file. The loop had no way to
notice and polled on to the deadline.

Detect it from the request's own on-disk state. A live request is either
unclaimed (`<id>.json`) or claimed by the dylib (`<id>.processing.<pid>`,
see processV2InboxFile). When neither exists and no reply has landed, the
queue was cleared and the request is gone, so surface `.bridgeNotReady`
immediately instead of stalling.

The outbox is re-checked once before giving up, because the dylib removes
its claim and writes the reply as separate steps and a reply can land
between the two checks. An inbox that cannot be enumerated is treated as
still-queued so a live request is never aborted by a transient read error.

Beyond ending the stall, this distinguishes "discarded, definitely not
delivered" from a plain timeout, which callers can safely retry — a
timeout leaves delivery genuinely unknown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEKzjCwQqoMEbZwabsY1u6
@omarshahine

Copy link
Copy Markdown
Contributor Author

@clawsweeper review

@clawsweeper

clawsweeper Bot commented Jul 28, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Command router queued. I will update this comment with the next step.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P1 Urgent regression or broken agent/channel workflow affecting real users now. merge-risk: 🚨 message-delivery 🚨 Merging this PR could drop, duplicate, misroute, suppress, or wrongly target messages. labels Jul 28, 2026
@clawsweeper

clawsweeper Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codex review: needs maintainer review before merge. Reviewed August 2, 2026, 1:44 AM ET / 05:44 UTC.

ClawSweeper review

What this changes

The branch makes a Messages bridge request fail promptly when a relaunch clears its queued file, while treating every vanished request without a response as delivery-unknown rather than retry-safe.

Merge readiness

⚠️ Ready for maintainer review - 2 items remain

This PR remains necessary: current main still waits only for a response file and can hold a discarded bridge request until its full timeout. The repaired head preserves the existing timeout error for every vanished request without a reply, resolving the earlier unsafe retry classification; no actionable patch finding remains.

Priority: P1
Reviewed head: 5c63215b1d5f3dcc920a160ef8ea568995e61910

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) The focused bridge reliability fix has credible real behavior proof, deterministic coverage, a maintainer-authored safety repair, and no remaining actionable correctness finding.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): The PR body contains redacted terminal proof from a real injected Messages.app restart showing prompt failure and recovery; the collaborator’s follow-up validates the repaired queue classifications without repeating a destructive relaunch. Keep any future logs redacted.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The PR body contains redacted terminal proof from a real injected Messages.app restart showing prompt failure and recovery; the collaborator’s follow-up validates the repaired queue classifications without repeating a destructive relaunch. Keep any future logs redacted.
Evidence reviewed 7 items Repository policy: The full repository policy asks for focused changes and deterministic regression coverage; this branch changes the bridge client plus a dedicated queue-state test suite.
Current-main gap: Current main polls only the response outbox until timeout. The PR head adds an inbox-state check after no response, so it can stop waiting when the request disappeared.
Queue-clearing mechanism: Launching injected Messages pre-creates then cleans both the v2 inbox and outbox directories, which can remove an in-flight request and its future response path.
Findings None None.
Security None None.

How this fits together

The iMessage bridge writes each CLI or gateway operation into a Messages-container inbox; the injected Messages helper claims it, performs the action, and writes a response into an outbox. The client poller returns that response to the caller, but a Messages relaunch can clear both queue directories while a request may be in flight.

flowchart LR
  A[CLI or gateway operation] --> B[Bridge client writes request]
  B --> C[Messages bridge inbox]
  C --> D[Injected Messages helper]
  D --> E[Response outbox]
  E --> F[Caller result]
  G[Messages relaunch] --> H[Queue directories cleared]
  H --> F
Loading

Before merge

  • Resolve merge risk (P2) - The timeout error can now be returned after one polling interval rather than after the configured full timeout when a request vanishes; this preserves its delivery-unknown meaning but changes timing for callers that had implicitly relied on waiting longer.
  • Complete next step (P2) - No focused automated repair remains; the contributor or maintainer only needs to clear the draft state and land the already-reviewed current head.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Patch scope 2 files affected; 188 added, 16 removed The branch confines behavior changes to the bridge client and its deterministic regression suite.
CI coverage 5 relevant checks successful The provided check state is green for macOS, core reads, workflow dispatch, and security scanning.

Merge-risk options

Maintainer options:

  1. Land the repaired timeout path (recommended)
    Merge the current head because it fails the vanished request promptly while retaining the established timeout outcome when delivery is unknown.
  2. Pause for timeout-timing policy
    Keep the PR open only if maintainers want to preserve the full timeout duration after a known queue-clear event despite the caller remaining blocked.

Technical review

Best possible solution:

Mark the repaired branch ready and merge it with the existing timeout contract intact, retaining the queue-state regression suite as protection for future bridge lifecycle changes.

Do we have a high-confidence way to reproduce the issue?

Yes. The PR supplies a real injected-Messages restart/fault-injection transcript, and source confirms that current main waits solely for the response file after a relaunch can remove the request.

Is this the best way to solve the issue?

Yes. The repaired head is the narrowest maintainable solution: detect a disappeared request to stop the stall, but return the existing delivery-unknown timeout rather than exposing a retry-safe or source-breaking new error case.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against e22dfad8e54e.

Labels

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR body contains redacted terminal proof from a real injected Messages.app restart showing prompt failure and recovery; the collaborator’s follow-up validates the repaired queue classifications without repeating a destructive relaunch. Keep any future logs redacted.
  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body contains redacted terminal proof from a real injected Messages.app restart showing prompt failure and recovery; the collaborator’s follow-up validates the repaired queue classifications without repeating a destructive relaunch. Keep any future logs redacted.
  • remove status: 📣 needs proof: Current PR status label is status: 👀 ready for maintainer look.
  • remove rating: 🦪 silver shellfish: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.

Label justifications:

  • P1: A Messages restart can make an active bridge operation wait for its full timeout, delaying real iMessage workflow recovery.
  • merge-risk: 🚨 compatibility: The patch intentionally changes the timing of an established timeout error for vanished requests.
  • merge-risk: 🚨 message-delivery: The branch governs whether callers can distinguish a request that was discarded from one whose delivery may already have occurred.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body contains redacted terminal proof from a real injected Messages.app restart showing prompt failure and recovery; the collaborator’s follow-up validates the repaired queue classifications without repeating a destructive relaunch. Keep any future logs redacted.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body contains redacted terminal proof from a real injected Messages.app restart showing prompt failure and recovery; the collaborator’s follow-up validates the repaired queue classifications without repeating a destructive relaunch. Keep any future logs redacted.

Evidence

What I checked:

  • Repository policy: The full repository policy asks for focused changes and deterministic regression coverage; this branch changes the bridge client plus a dedicated queue-state test suite. (AGENTS.md:1, e22dfad8e54e)
  • Current-main gap: Current main polls only the response outbox until timeout. The PR head adds an inbox-state check after no response, so it can stop waiting when the request disappeared. (Sources/IMsgCore/IMsgBridgeClient.swift:97, 5c63215b1d5f)
  • Queue-clearing mechanism: Launching injected Messages pre-creates then cleans both the v2 inbox and outbox directories, which can remove an in-flight request and its future response path. (Sources/IMsgCore/MessagesLauncher.swift:120, e22dfad8e54e)
  • Producer ordering: The injected helper atomically renames its reply into the outbox before dropping the processing claim; the client response recheck therefore covers a reply that lands at the observation boundary. (Sources/IMsgHelper/IMsgInjected.m:6733, e22dfad8e54e)
  • Prior blocker repaired: At the current head, an absent request is mapped to the existing timeout case because an unseen claim could have been created, acted on, and removed between polls; this resolves the prior retry-safety and public-enum concerns without changing the public error enum. (Sources/IMsgCore/IMsgBridgeClient.swift:123, 5c63215b1d5f)
  • Deterministic regression coverage: The new suite covers unclaimed, claimed, absent, unreadable, and both histories that end absent, documenting why absence cannot prove retry safety. (Tests/IMsgCoreTests/IMsgBridgeClientQueueTests.swift:74, 5c63215b1d5f)

Likely related people:

  • Peter Steinberger: Current-main blame attributes the existing v2 wait loop to Peter, and he authored the current PR-head repair that keeps the established timeout contract. (role: recent area contributor; confidence: high; commits: 1d8b679cc3a3, 5c63215b1d5f; files: Sources/IMsgCore/IMsgBridgeClient.swift, Sources/IMsgCore/MessagesLauncher.swift)
  • Omar Shahine: Omar contributed prior merged bridge work and authored the original implementation plus the dedicated queue-state test suite on this branch. (role: bridge contributor; confidence: high; commits: 1ee36ddec1fa, 94b58118b5af, 1f87ec96563e; files: Sources/IMsgCore/IMsgBridgeClient.swift, Tests/IMsgCoreTests/IMsgBridgeClientQueueTests.swift)

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (20 earlier review cycles; latest 8 shown)
  • reviewed 2026-07-31T09:33:26.340Z sha 1f87ec9 :: found issues before merge. :: [P1] Preserve delivery unknown across an unseen claim | [P1] Preserve compatibility for exhaustive error switches
  • reviewed 2026-07-31T16:01:33.799Z sha 1f87ec9 :: found issues before merge. :: [P1] Preserve delivery unknown across an unseen claim | [P1] Resolve compatibility for the new public error case
  • reviewed 2026-08-01T05:09:32.978Z sha 1f87ec9 :: needs real behavior proof before merge. :: [P1] Preserve delivery unknown across an unseen claim | [P1] Preserve compatibility for exhaustive error switches
  • reviewed 2026-08-01T11:55:02.776Z sha 1f87ec9 :: needs real behavior proof before merge. :: [P1] Fail closed when a claim can disappear between polls | [P1] Avoid extending the public error enum without compatibility handling
  • reviewed 2026-08-01T13:13:19.463Z sha 1f87ec9 :: needs real behavior proof before merge. :: [P1] Treat an unseen claim as delivery-unknown | [P1] Preserve compatibility for exhaustive error switches
  • reviewed 2026-08-02T00:16:47.494Z sha 1f87ec9 :: needs real behavior proof before merge. :: [P1] Treat unobserved claims as delivery-unknown | [P1] Preserve compatibility for the public error enum
  • reviewed 2026-08-02T02:41:44.796Z sha 1f87ec9 :: needs real behavior proof before merge. :: [P1] Fail closed when a claim is missed between polls | [P1] Preserve compatibility for exhaustive error switches
  • reviewed 2026-08-02T05:08:33.280Z sha 1f87ec9 :: needs real behavior proof before merge. :: [P1] Treat an unobserved claim as delivery-unknown | [P1] Avoid an implicit public error-enum break

@omarshahine

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 28, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Command router queued. I will update this comment with the next step.

@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jul 28, 2026
Review raised that a vanished request can be reported as definitely-not-
delivered even though the action may already have run. The stated mechanism
(claim removed before the reply is written) is inverted — processV2InboxFile
renames the reply into the outbox and only then drops the claim — but the
concern is real through a different path: if the dylib dies after IMCore
delivered and before the reply is published, the claim is orphaned and a
later scan or relaunch clears it, exactly as that function's own comment
describes.

The defect was in the error semantics, not the ordering. Both situations
collapsed into `.bridgeNotReady`, which the PR documents as retry-safe.

Split them by tracking whether the dylib ever claimed the request:

- never claimed, then absent -> nothing read it, so the action did not run.
  Still `.bridgeNotReady`, still retry-safe.
- claimed, then absent with no reply -> the dylib had it and died mid-flight.
  New `.deliveryUnknown(action:)`, which callers must not retry blind.

`requestStillQueued` becomes `requestQueueState` returning
unclaimed/claimed/absent. An inbox that cannot be enumerated still reports
unclaimed so a transient read error never ends a live request.

Adds the interleaving test the review asked for, walking one request through
unclaimed -> absent and another through unclaimed -> claimed -> absent, plus
a guard that the two error cases stay distinct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEKzjCwQqoMEbZwabsY1u6
@omarshahine

Copy link
Copy Markdown
Contributor Author

Thanks — the finding pointed at a real problem, though not via the mechanism described. Fixed in 1f87ec9.

On the stated ordering. The review says the producer "removes its processing claim and writes its reply as separate steps, leaving an interval where the action may already have run but the reply is not yet visible." The order is the opposite. In processV2InboxFile the reply is renamed into the outbox before the claim is dropped:

response = processV2Envelope(envelope);      // action executes
...
[responseData writeToFile:tmp atomically:NO];
rename(tmp.UTF8String, outPath.UTF8String);  // reply published (atomic)

// Drop the claimed request — we're done with it. If the process dies
// after claiming, a later inbox scan removes the orphan without
// replaying a potentially delivered side effect.
[[NSFileManager defaultManager] removeItemAtPath:claimPath error:nil];

So "keep the processing claim until the reply is published" is already the invariant, and reordering would be a no-op.

But the concern is valid through another path, which that comment names: if the dylib dies after IMCore delivered and before the reply is written, the claim is orphaned, and cleanupOrphanedV2Claims or a relaunch clears it. My check then saw nothing and reported .bridgeNotReady, which this PR documents as retry-safe — for a message that may well have gone out. On the machine this was found on, Messages.app dies every 2-3 days, so that window is not hypothetical.

The defect was my error semantics, not the producer ordering: I collapsed two different situations into one retry-safe error.

Fix. Track whether the dylib ever claimed the request:

Observed Meaning Error
never claimed → absent nothing read it, action did not run .bridgeNotReady (retry-safe, unchanged)
claimed → absent, no reply dylib died mid-request, action may have run .deliveryUnknown(action:) — must not be retried blind

requestStillQueued became requestQueueState returning unclaimed / claimed / absent. An inbox that cannot be enumerated still reports unclaimed, so a transient read error never ends a live request.

Added the interleaving test that was asked for: one request walked through unclaimed → absent, another through unclaimed → claimed → absent, plus a guard that the two error cases cannot be conflated.

swift test: 491 tests in 4 suites passing. swift format lint and swiftlint clean on all three touched files; git diff --check clean.

Two things I did not change, deliberately:

  • No producer-side reordering, since the invariant already holds. If you would still prefer a durable producer-side record (e.g. keeping the claim until the reply is confirmed readable), say so and I will do it — but it looked like added protocol surface for an invariant that is already satisfied.
  • The residual window is narrowed, not closed. A crash between delivery and reply publication is still unobservable from the client; the change makes that case report unknown rather than retry-safe, which is the honest outcome rather than a guarantee.

@omarshahine

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 28, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Command router queued. I will update this comment with the next step.

@omarshahine

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 28, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

Re-review progress:

@omarshahine

Copy link
Copy Markdown
Contributor Author

Note for anyone reading this PR: the review shown above is stale. It was produced against 94b5811 and predates the fix for its own P1.

The P1 ("Keep the processing claim until the reply is published") was addressed in 1f87ec9, along with the interleaving test it asked for. Summary of that exchange, in case the bot verdict never refreshes:

  • The finding's stated mechanism is inverted. processV2InboxFile renames the reply into the outbox before dropping the claim, so the requested ordering is already the invariant and reordering would be a no-op. Quoted source is in the reply above.
  • The underlying concern is real via a different path, which that function's own comment names: if the dylib dies after IMCore delivered but before the reply is written, the claim is orphaned and later cleared. My original code reported that as retry-safe .bridgeNotReady.
  • Fixed by splitting the error semantics rather than the ordering: never-claimed-then-absent stays .bridgeNotReady (retry-safe); claimed-then-absent-with-no-reply becomes the new .deliveryUnknown(action:), which must not be retried blind.

State at 1f87ec9: swift test 491 tests in 4 suites passing, swift format lint and swiftlint clean on all touched files, git diff --check clean. Proof of the original stall fix was accepted in the last review (proof: sufficient).

Bot-side observation, in case it is useful to whoever maintains ClawSweeper: re-review requests are acking within seconds but not producing a review, and it only started after the head moved. Reviews ran fine at 94b5811 (ack 06:32 → verdict 06:38; ack 14:50 → verdict 15:23), and all three requests since the push to 1f87ec9 have acked and gone quiet (19:08, 22:15, plus the automatic trigger on the body update). Not re-issuing the command to avoid piling on a possibly-active lease.

@clawsweeper clawsweeper Bot added merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. and removed rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Jul 29, 2026
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. proof: sufficient Contributor real behavior proof is sufficient. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Jul 31, 2026
@steipete

steipete commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Maintainer repair is on the branch at 5c63215; I recommend landing this PR once CI is green.

The fast-failure mechanism is sound, but the original branch inferred retry safety from whether this client happened to observe a .processing file. That state can be created, acted on, and removed entirely between two 50ms polls, so “never observed claimed” did not prove the send was safe to retry. The repair treats every vanished request without a reply as delivery-unknown via the existing timeout case, preserving source compatibility and preventing blind duplicate sends.

Proof on macOS arm64:

  • Queue-state suite: all 5 tests passed, covering unclaimed, claimed, absent, unreadable, and both absent-state histories.
  • make lint: passed with pre-existing warnings only.
  • make test: all 490 tests passed after the known attachment-descendant PID-file flake failed once; its exact retry and the complete retry passed.
  • make build ARCHES="$(uname -m)": release CLI and universal helper built successfully.
  • Live release status run succeeded and reported 0.13.5, SIP enabled, v2_ready=false, and basic features available. That host state makes a second destructive Messages relaunch/fault-injection run inappropriate; the contributor’s real bridge restart reproduction on this PR remains the live proof of the fast-failure path. The maintainer repair changes only retry classification after that same disappearance.
  • Local-diff and full-branch autoreviews: clean, no actionable findings.

No message was sent and Messages.app was not relaunched during maintainer verification.

@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Aug 2, 2026
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
@steipete
steipete marked this pull request as ready for review August 2, 2026 06:32
@steipete
steipete merged commit 8c34bc4 into openclaw:main Aug 2, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. merge-risk: 🚨 message-delivery 🚨 Merging this PR could drop, duplicate, misroute, suppress, or wrongly target messages. P1 Urgent regression or broken agent/channel workflow affecting real users now. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants